/* *******************************************************************
 * Copyright (c) 2003 Contributors
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Wes Isberg     initial implementation
 * ******************************************************************/

package org.aspectj.testing.drivers;

import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.aspectj.ajdt.internal.core.builder.AjState;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHolder;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.testing.harness.bridge.AjcTest;
import org.aspectj.testing.harness.bridge.AjcTest.Spec;
import org.aspectj.testing.harness.bridge.RunSpecIterator;
import org.aspectj.testing.harness.bridge.Sandbox;
import org.aspectj.testing.harness.bridge.Validator;
import org.aspectj.testing.run.IRunIterator;
import org.aspectj.testing.run.RunStatus;
import org.aspectj.testing.run.Runner;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestResult;
import junit.framework.TestSuite;

/*
 * Adapt Harness tests to JUnit driver. This renders suite files as TestSuite
 * and AjcTest as TestCase. When run, aborts are reported as error and fails as
 * failures, with all messages stuffed into the one JUnit exception message.
 * Test options are supported, but no harness options. The TestSuite
 * implementation prevents us from re-running tests. In the Eclipse JUnit test
 * runner, the tests display hierarchically and with annotations and with
 * messages. You can stop the tests, but not traverse to the source or re-run
 * the test.
 */
public class AjctestsAdapter extends TestSuite {
	public static final String VERBOSE_NAME = AjctestsAdapter.class.getName()
			+ ".VERBOSE";

	private static final boolean VERBOSE = HarnessJUnitUtil
			.readBooleanSystemProperty(VERBOSE_NAME);

	/**
	 * Factory to make and populate suite without options.
	 *
	 * @param suitePath
	 *            the String path to a harness suite file
	 * @return AjctestJUnitSuite populated with tests
	 */
	public static AjctestsAdapter make(String suitePath) {
		return make(suitePath, null);
	}

	/**
	 * Factory to make and populate suite
	 *
	 * @param suitePath
	 *            the String path to a harness suite file
	 * @param options
	 *            the String[] options to use when creating tests
	 * @return AjctestJUnitSuite populated with tests
	 */
	public static AjctestsAdapter make(String suitePath, String[] options) {
		AjctestsAdapter result = new AjctestsAdapter(suitePath, options);
		AjcTest.Spec[] tests = AjcTest.Suite.getTests(result.getSpec());
		if (VERBOSE) {
			log("loading " + tests.length + " tests in " + suitePath);
		}
		for (Spec ajcTest : tests) {
			result.addTest(new AjcTestSpecAsTest(ajcTest, result));
		}
		return result;
	}

	private static void log(String message) {
		System.err.println(message);
		System.err.flush();
	}

	private final String suitePath;

	private final String[] options;

	private AjcTest.Suite.Spec spec;

	private Runner runner;

	private Validator validator;

	private IMessageHolder holder;

	private Sandbox sandbox;

	private File suiteDir;

	private String name;

	private AjctestsAdapter(String suitePath, String[] options) {
		this.suitePath = suitePath;
		String[] opts = new String[0];
		if (!HarnessJUnitUtil.isEmpty(options)) {
			opts = new String[options.length];
			System.arraycopy(options, 0, opts, 0, opts.length);
		}
		this.options = opts;
	}

	@Override
	public void addTest(Test test) {
		if (!(test instanceof AjcTestSpecAsTest)) {
			String m = "expecting AjcTestSpecAsTest, got "
					+ (null == test ? "null test" : test.getClass().getName()
							+ ": " + test);
			throw new IllegalArgumentException(m);
		}
		super.addTest(test);
	}

	@SuppressWarnings("rawtypes")
	@Override
	public void addTestSuite(Class testClass) {
		throw new Error("unimplemented");
	}

	@Override
	public String getName() {
		if (null == name) {
			name = HarnessJUnitUtil.cleanTestName(suitePath
					+ Arrays.asList(options));
		}
		return name;
	}

	/**
	 * Callback from test to run it using suite-wide holder, etc. The caller is
	 * responsible for calling result.startTest(test) and result.endTest(test);
	 *
	 * @param test
	 *            the AjcTestSpecAsTest to run
	 * @param result
	 *            the TestResult for any result messages (may be null)
	 */
	protected void runTest(AjcTestSpecAsTest test, TestResult result) {
		final Runner runner = getRunner();
		final Sandbox sandbox = getSandbox();
		final Validator validator = getValidator();
		int numIncomplete = 0;
		final RunStatus status = new RunStatus(new MessageHandler(), runner);
		status.setIdentifier(test.toString());
		try {
			IMessageHolder holder = getHolder();
			holder.clearMessages();
			IRunIterator steps = test.spec.makeRunIterator(sandbox, validator);
			if (0 < holder.numMessages(IMessage.ERROR, true)) {
				MessageUtil.handleAll(status, holder, IMessage.INFO, true,
						false);
			} else {
				runner.runIterator(steps, status, null);
			}
			if (steps instanceof RunSpecIterator) {
				numIncomplete = ((RunSpecIterator) steps).getNumIncomplete();
			}
		} finally {
			try {
				// reportResult handles null TestResult
				HarnessJUnitUtil
				.reportResult(null, status, test, numIncomplete);
			} finally {
				validator.deleteTempFiles(true);
			}
		}
	}

	private File getSuiteDir() {
		if (null == suiteDir) {
			File file = new File(suitePath);
			file = file.getParentFile();
			if (null == file) {
				file = new File(".");
			}
			suiteDir = file;
		}
		return suiteDir;
	}

	private Validator getValidator() {
		if (null == validator) {
			validator = new Validator(getHolder());
			// XXX lock if keepTemp?
		}
		return validator;
	}

	private Runner getRunner() {
		if (null == runner) {
			runner = new Runner();
		}
		return runner;
	}

	private IMessageHolder getHolder() {
		if (null == holder) {
			holder = new MessageHandler();
		}
		return holder;
	}

	private AjcTest.Suite.Spec getSpec() {
		if (null == spec) {
			IMessageHolder holder = getHolder();
			spec = HarnessJUnitUtil.getSuiteSpec(suitePath, options,
					getHolder());
			if (VERBOSE && holder.hasAnyMessage(null, true)) {
				MessageUtil.print(System.err, holder, "skip ",
						MessageUtil.MESSAGE_MOST);
			}
			holder.clearMessages();
		}
		return spec;
	}

	private Sandbox getSandbox() {
		if (null == sandbox) {
			sandbox = new Sandbox(spec.getSuiteDirFile(), getValidator());
		}
		return sandbox;
	}

	/**
	 * Wrap AjcTest.Spec for lookup by description
	 *
	 * @author wes
	 */
	public static class SpecTests {
		private static final HashMap<String,SpecTests> TESTS = new HashMap<>();

		//        private static void putSpecTestsFor(String id, SpecTests tests) {
		//            TESTS.put(id, tests);
		//        }

		private static SpecTests getSpecTestsFor(String id) {
			SpecTests result = TESTS.get(id);
			if (null == result) {
				throw new Error("no tests found for " + id);
			}
			return result;
		}

		// ------------------------------------
		final AjctestsAdapter mAjctestsAdapter;

		private final Map<String,AjcTest.Spec> mDescriptionToAjcTestSpec;

		// ------------------------------------
		private SpecTests(AjctestsAdapter ajctestsAdapter, AjcTest.Spec[] tests) {
			mAjctestsAdapter = ajctestsAdapter;
			Map<String,AjcTest.Spec> map = new HashMap<>();
			for (Spec test : tests) {
				map.put(test.getDescription(), test);
			}

			mDescriptionToAjcTestSpec = Collections.unmodifiableMap(map);
		}

		/**
		 * @param description
		 *            the String description of the test
		 * @throws IllegalArgumentException
		 *             if testName is not found
		 */
		protected void runTest(String description) {
			AjcTest.Spec spec = getSpec(description);
			AjctestsAdapter.AjcTestSpecAsTest ajcTestAsSpec = new AjctestsAdapter.AjcTestSpecAsTest(
					spec, mAjctestsAdapter);
			// runTest handles null TestResult
			mAjctestsAdapter.runTest(ajcTestAsSpec, null);
		}

		/**
		 * @param description
		 *            the String description of the test
		 * @throws IllegalArgumentException
		 *             if testName is not found
		 */
		private AjcTest.Spec getSpec(String description) {
			AjcTest.Spec spec = mDescriptionToAjcTestSpec
					.get(description);
			if (null == spec) {
				throw new IllegalArgumentException("no test for " + description);
			}
			return spec;
		}

		/**
		 * makeUsingTestClass(..) extends this to create TestCase with
		 * test_{name} for each test case.
		 */
		public static class TestClass extends TestCase {
			public TestClass() {
			}
			private SpecTests mTests;

			/**
			 * Called by code generated in makeUsingTestClass(..)
			 *
			 * @param description
			 *            the String identifier of the test stored in SpecTests
			 *            mTests.
			 * @throws Error
			 *             on first and later uses if getTestsFor() returns
			 *             null.
			 */
			public final void runTest(String description) {
				if (null == mTests) {
					String classname = getClass().getName();
					mTests = getSpecTestsFor(classname);
				}
				mTests.runTest(description);
			}
		}
	}

	/** Wrap AjcTest.Spec as a TestCase. Run by delegation to suite */
	private static class AjcTestSpecAsTest extends TestCase implements
	HarnessJUnitUtil.IHasAjcSpec {
		// this could implement Test, but Ant batchtest fails to pull name
		final String name;

		final AjcTest.Spec spec;

		AjctestsAdapter suite;

		AjcTestSpecAsTest(AjcTest.Spec spec, AjctestsAdapter suite) {
			super(HarnessJUnitUtil.cleanTestName(spec.getDescription()));
			this.name = HarnessJUnitUtil.cleanTestName(spec.getDescription());
			this.suite = suite;
			this.spec = spec;
			spec.setSuiteDir(suite.getSuiteDir());
		}

		@Override
		public int countTestCases() {
			return 1;
		}

		@Override
		public AjcTest.Spec getAjcTestSpec() {
			return spec;
		}

		@Override
		public void run(TestResult result) {
			if (null == suite) {
				throw new Error("need to re-init");
			}
			try {
				AjState.FORCE_INCREMENTAL_DURING_TESTING = true;
				result.startTest(this);
				suite.runTest(this, result);
			} finally {
				result.endTest(this);
				suite = null;
				AjState.FORCE_INCREMENTAL_DURING_TESTING = false;
			}
		}

		@Override
		public String getName() {
			return name;
		}

		@Override
		public String toString() {
			return name;
		}
	}
}
